<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>乡村教师AI工具包 · Chatbot文生图</title>
  <!-- Pico.css v2 官方 CDN -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
  <style>
    body{padding:2rem}
    #app,.tab-content{display:none}
    .tab-content.active{display:block;margin-top:1.5rem}
    dialog{width:90%;max-width:520px}
    .output-area{background:var(--pico-secondary-background);padding:1rem;border-radius:var(--pico-border-radius);margin-top:1rem;white-space:pre-wrap;word-break:break-word;min-height:150px}
    .loading{display:inline-block;border:4px solid var(--pico-muted-border-color);border-top:4px solid var(--pico-primary);border-radius:50%;width:24px;height:24px;animation:spin 1s linear infinite}
    @keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
    .chat{max-height:520px;overflow:auto;border:1px solid var(--pico-muted-border-color);border-radius:var(--pico-border-radius);padding:1rem}
    .msg{margin:.5rem 0}
    .msg.user{color:var(--pico-color);}
    .msg.assistant{}
    .msg img{max-width:100%;border-radius:.5rem;margin-top:.5rem}
    .row{display:flex;gap:.75rem;flex-wrap:wrap}
    .grow{flex:1 1 260px}
    .muted{color:var(--pico-muted-color)}
    .pill{display:inline-flex;gap:.5rem;align-items:center}
    .small{font-size:.9rem}
    .kbd{font-family:monospace;background:var(--pico-secondary-background);padding:.25rem .5rem;border-radius:.35rem;border:1px solid var(--pico-muted-border-color)}
  </style>
</head>
<body>
<main class="container">

  <!-- 🔐 API Key 门禁 -->
  <dialog id="apiKeyModal">
    <article>
      <header><h3>请输入您的 API Key</h3></header>
      <p>为使用本工具，请先提供有效的访问令牌。</p>
      <ul>
        <li>ModelScope（必填）：<a href="https://www.modelscope.cn/my/myaccesstoken" target="_blank" rel="noopener">https://www.modelscope.cn/my/myaccesstoken</a></li>
        <li>Tavily（可选，用于更强网页搜索）：<a href="https://docs.tavily.com/" target="_blank" rel="noopener">https://docs.tavily.com</a></li>
      </ul>
      <form id="apiKeyForm">
        <label>ModelScope API Key
          <input type="password" id="msKey" placeholder="sk-..." required />
        </label>
        <label>Tavily API Key（可选）
          <input type="password" id="tvKey" placeholder="tvly-..." />
        </label>
        <footer class="row">
          <button type="submit">保存并进入</button>
          <button type="button" class="secondary" onclick="navigator.clipboard.readText().then(t=>{msKey.value=t})">从剪贴板粘贴</button>
        </footer>
        <p id="keyErr" class="muted" style="color:var(--pico-del-color)"></p>
      </form>
    </article>
  </dialog>

  <div id="app">
    <header>
      <h1>乡村教师AI工具包 · Chatbot文生图</h1>
      <p class="muted">用于给孩子科普 AI 技术，并用 AI 辅助学习课本知识；服务乡村教师，减轻备课与讲解负担。</p>
    </header>

    <!-- 顶部模型与设置 -->
    <section class="row">
      <div class="grow">
        <label>对话/多模态模型（VLM）</label>
        <input id="chatModel" value="Qwen/Qwen3-VL-30B-A3B-Instruct" />
      </div>
      <div class="grow">
        <label>文生图模型</label>
        <input id="imgModel" value="black-forest-labs/FLUX.1-Krea-dev" />
      </div>
      <div>
        <label>&nbsp;</label>
        <button id="btnChangeKey" class="secondary">更换 API Key</button>
      </div>
    </section>
    <p class="muted small">API Base：<span class="kbd">https://api-inference.modelscope.cn/v1</span> · 模型名可直接修改。</p>

    <!-- 导航 -->
    <nav>
      <ul>
        <li><button class="tab-button active" data-tab="chat">🗨️ 体验AI对话 / 生图</button></li>
        <li><button class="tab-button" data-tab="web2img">🔎 主动上网 → 生图</button></li>
        <li><button class="tab-button" data-tab="about">ℹ️ 使用须知</button></li>
      </ul>
    </nav>

    <!-- Tab: 对话 / 生图 -->
    <section id="chat" class="tab-content active">
      <div class="chat" id="chatList" aria-live="polite"></div>

      <form id="chatForm" class="row" style="margin-top:.75rem">
        <select id="mode" class="small">
          <option value="chat">对话</option>
          <option value="img">生图</option>
        </select>
        <input id="chatInput" class="grow" placeholder="给孩子讲讲“什么是机器学习”？或输入生图提示词…" />
        <button type="submit">发送</button>
      </form>

      <details style="margin-top:.5rem">
        <summary>高级参数</summary>
        <div class="row">
          <label class="grow">temperature <input id="temp" type="number" step="0.1" value="0.7"></label>
          <label class="grow">max_tokens <input id="maxtok" type="number" value="1024"></label>
          <label class="grow">生成张数（生图） <input id="imgN" type="number" value="1" min="1" max="4"></label>
          <label class="grow">响应格式 <select id="imgFmt"><option value="b64_json">base64</option><option value="url">URL</option></select></label>
        </div>
      </details>
      <p class="muted small">提示：输入框前的下拉可切换「对话 / 生图」。</p>
    </section>

    <!-- Tab: 主动上网 → 生图 -->
    <section id="web2img" class="tab-content">
      <form id="searchForm" class="row">
        <input id="kw" class="grow" placeholder="输入要科普的关键词，如：光合作用 / 牛顿三大定律 / 计算机视觉" />
        <select id="provider">
          <option value="wikipedia">Wikipedia（默认，无需Key）</option>
          <option value="tavily">Tavily（需API Key）</option>
        </select>
        <button type="submit">检索并生成海报</button>
      </form>
      <div class="output-area" id="searchOut">输入关键词后，这里会展示检索结果摘要与生成的教学海报。</div>
    </section>

    <!-- Tab: 关于 -->
    <section id="about" class="tab-content">
      <article>
        <h3>使用须知与教学提示</h3>
        <ul>
          <li>本页直接调用 ModelScope 的 OpenAI 兼容 API（Chat & Images）。:contentReference[oaicite:5]{index=5}</li>
          <li>搜索默认走 Wikipedia REST API，支持浏览器 CORS；也可选 Tavily。:contentReference[oaicite:6]{index=6}</li>
          <li>科普对话的系统提示词参考了 Datawhale 的提示词工程实践：角色设定、结构化输出、格式约束等。:contentReference[oaicite:7]{index=7}</li>
        </ul>
      </article>
    </section>
  </div>
</main>

<script>
document.addEventListener('DOMContentLoaded', init);

function init(){
  // --- 0. Gate ---
  const modal = document.getElementById('apiKeyModal');
  const msKey = document.getElementById('msKey');
  const tvKey = document.getElementById('tvKey');
  const app = document.getElementById('app');
  const saved = sessionStorage.getItem('MS_KEY');
  if(!saved){ modal.showModal(); }
  else { app.style.display='block'; }

  document.getElementById('apiKeyForm').addEventListener('submit', (e)=>{
    e.preventDefault();
    const k = msKey.value.trim();
    if(!k){ document.getElementById('keyErr').textContent='请填写 ModelScope API Key'; return; }
    sessionStorage.setItem('MS_KEY', k);
    const tk = tvKey.value.trim();
    if(tk) sessionStorage.setItem('TV_KEY', tk);
    modal.close(); app.style.display='block';
  });
  document.getElementById('btnChangeKey').onclick = ()=>{ modal.showModal(); };

  // --- 1. Tabs ---
  const tabBtns = document.querySelectorAll('.tab-button');
  const tabs = document.querySelectorAll('.tab-content');
  tabBtns.forEach(btn=>{
    btn.addEventListener('click', ()=>{
      tabBtns.forEach(b=>b.classList.remove('active'));
      btn.classList.add('active');
      const id = btn.dataset.tab;
      tabs.forEach(t=> t.classList.toggle('active', t.id===id));
    });
  });

  // --- 2. Chat / Img ---
  const chatList = document.getElementById('chatList');
  const chatForm = document.getElementById('chatForm');
  const chatInput = document.getElementById('chatInput');
  const modeSel = document.getElementById('mode');

  const chatModel = document.getElementById('chatModel');
  const imgModel = document.getElementById('imgModel');
  const temp = document.getElementById('temp');
  const maxtok = document.getElementById('maxtok');
  const imgN = document.getElementById('imgN');
  const imgFmt = document.getElementById('imgFmt');

  const sysPrompt = `你是“乡村教师AI助手”，面向8-15岁孩子科普AI/课本知识。
- 用“生活化类比 + 分步骤讲解 + 小练习”结构输出
- 不使用危险或成人内容
- 输出末尾用一句“给老师的建议”提示课堂组织方式
- Markdown 输出，标题清晰
`;

  let history = [{role:'system', content: sysPrompt}];

  chatForm.addEventListener('submit', async (e)=>{
    e.preventDefault();
    const text = chatInput.value.trim();
    if(!text) return;
    const mode = modeSel.value; // chat | img
    addMsg('user', text);
    chatInput.value='';

    if(mode === 'chat'){
      addLoading();
      const r = await callChat({
        model: chatModel.value.trim(),
        temperature: Number(temp.value||0.7),
        max_tokens: Number(maxtok.value||1024),
        messages: [...history, {role:'user', content:text}]
      });
      removeLoading();
      const content = r.ok ? pickContent(r.data) : `❌ ${r.err}`;
      addMsg('assistant', content);
      if(r.ok) history.push({role:'user',content:text}, {role:'assistant',content:content});
    }else{
      addLoading();
      const r = await callImages({
        model: imgModel.value.trim(),
        prompt: text,
        n: Number(imgN.value||1),
        response_format: imgFmt.value
      });
      removeLoading();
      if(!r.ok){ addMsg('assistant', '❌ '+r.err); return; }
      showImages(r.data);
    }
  });

  // --- 3. Web → Img ---
  const searchForm = document.getElementById('searchForm');
  const searchOut = document.getElementById('searchOut');
  searchForm.addEventListener('submit', async (e)=>{
    e.preventDefault();
    const kw = document.getElementById('kw').value.trim();
    const prov = document.getElementById('provider').value;
    if(!kw){ alert('请输入关键词'); return; }
    searchOut.innerHTML = `<span class="loading"></span> 正在检索 ${kw} …`;

    try{
      let summary = '';
      if(prov==='tavily' && sessionStorage.getItem('TV_KEY')){
        const res = await fetch('https://api.tavily.com/search', {
          method:'POST',
          headers:{'Content-Type':'application/json','Authorization':`Bearer ${sessionStorage.getItem('TV_KEY')}`},
          body: JSON.stringify({query: kw, include_answer:true, max_results:5})
        });
        if(!res.ok) throw new Error('Tavily 搜索失败');
        const j = await res.json();
        summary = (j.answer || j.results?.[0]?.content || '').slice(0, 1000);
      }else{
        // Wikipedia：先搜索，再取摘要
        const s = await fetch(`https://zh.wikipedia.org/w/rest.php/v1/search/page?q=${encodeURIComponent(kw)}&limit=1`);
        if(!s.ok) throw new Error('Wikipedia 搜索失败');
        const sj = await s.json();
        const title = sj.pages?.[0]?.title || kw;
        const sum = await fetch(`https://zh.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}?redirect=true`);
        if(!sum.ok) throw new Error('Wikipedia 摘要失败');
        const data = await sum.json();
        summary = (data.extract || '').slice(0, 1000);
      }

      if(!summary){ searchOut.textContent='未获取到摘要，请换个关键词重试。'; return; }

      // 生成教学海报（文生图）
      const prompt = `为孩子制作一张中文“知识海报”，主题：${kw}。
请根据下列科普摘要设计：${summary}
风格：清晰板书/手绘信息图，包含：标题、3-5个要点（每点配小图标）、一个课堂小练习（1题），色彩温和，适合黑白打印可读。`;
      searchOut.innerHTML = `📚 摘要：${summary}\n\n🖼️ 正在根据摘要生成教学海报…`;
      const gen = await callImages({
        model: imgModel.value.trim(),
        prompt: prompt,
        n: 1,
        response_format: 'b64_json'
      });
      if(!gen.ok){ searchOut.textContent = '❌ 生图失败：'+gen.err; return; }
      const url = toObjectURL(gen.data);
      searchOut.innerHTML = `📚 摘要：${summary}\n\n<img src="${url}" alt="教学海报" />\n\n<a href="${url}" download="poster.png">下载图片</a>`;
    }catch(err){
      searchOut.textContent = '❌ '+err.message;
    }
  });

  // --- helpers: chat & image ---
  function msHeaders(){
    const key = sessionStorage.getItem('MS_KEY');
    if(!key) throw new Error('未设置 ModelScope API Key');
    return {'Content-Type':'application/json','Authorization':`Bearer ${key}`};
  }
  async function callChat(payload){
    try{
      const r = await fetch('https://api-inference.modelscope.cn/v1/chat/completions', {
        method:'POST', headers: msHeaders(), body: JSON.stringify(payload)
      });
      const txt = await r.text();
      if(!r.ok) return {ok:false, err: txt};
      return {ok:true, data: JSON.parse(txt)};
    }catch(e){ return {ok:false, err:e.message}; }
  }
  async function callImages(payload){
    try{
      const r = await fetch('https://api-inference.modelscope.cn/v1/images/generations', {
        method:'POST', headers: msHeaders(), body: JSON.stringify(payload)
      });
      const txt = await r.text();
      if(!r.ok) return {ok:false, err: txt};
      return {ok:true, data: JSON.parse(txt)};
    }catch(e){ return {ok:false, err:e.message}; }
  }
  function pickContent(resp){
    // OpenAI 兼容：choices[0].message.content（字符串或富文本）
    const c = resp?.choices?.[0]?.message?.content;
    if(!c) return '';
    return Array.isArray(c) ? c.map(p=> typeof p==='string'?p:(p.text||'')).join('') : (typeof c==='string'?c:(c.text||''));
  }
  function toObjectURL(imageResp){
    // 兼容 b64_json / url
    const item = imageResp?.data?.[0];
    if(!item) return '';
    if(item.url) return item.url;
    if(item.b64_json){
      const bin = atob(item.b64_json);
      const u8 = new Uint8Array(bin.length); for(let i=0;i<bin.length;i++) u8[i]=bin.charCodeAt(i);
      return URL.createObjectURL(new Blob([u8], {type:'image/png'}));
    }
    return '';
  }

  // --- UI helpers ---
  function addMsg(role, text){
    const div = document.createElement('div');
    div.className = 'msg '+role;
    div.innerHTML = `<b>${role==='user'?'我':'助手'}</b>：<div>${escapeHtml(text).replace(/\n/g,'<br/>')}</div>`;
    chatList.appendChild(div); chatList.scrollTop = chatList.scrollHeight;
  }
  function showImages(resp){
    const item = resp?.data?.[0];
    if(!item){ addMsg('assistant', '未返回图片'); return; }
    const url = toObjectURL(resp);
    const div = document.createElement('div');
    div.className = 'msg assistant';
    div.innerHTML = `<b>助手</b>：<div>已生成图片：</div><img src="${url}" alt="生成结果"/><div><a href="${url}" download="gen.png">下载图片</a></div>`;
    chatList.appendChild(div); chatList.scrollTop = chatList.scrollHeight;
  }
  function addLoading(){
    const div = document.createElement('div'); div.className='msg assistant'; div.id='loading';
    div.innerHTML = `<span class="loading"></span> 正在思考/生成…`;
    chatList.appendChild(div); chatList.scrollTop = chatList.scrollHeight;
  }
  function removeLoading(){
    const el = document.getElementById('loading'); if(el) el.remove();
  }
  function escapeHtml(s){ return s.replace(/[&<>"']/g, m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m])); }
}
</script>
</body>
</html>
